﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.IO.Compression;
using ProtoBuf;
using System.Diagnostics.Contracts;
using System.Drawing;
using System.Drawing.Imaging;

namespace RPGMapRevealer
{
    class MapInfoSerializer
    {
        public static MapInfo Deserialize(String file, DirectoryInfo tileFolder, bool isCompressed = true)
        {
            Contract.Requires(file != null);
            Contract.Requires(tileFolder != null);

            return Deserialize(new FileInfo(file), tileFolder, isCompressed);
        }

        public static MapInfo Deserialize(FileInfo file, DirectoryInfo tileFolder, bool isCompressed = true)
        {
            Contract.Requires(file != null);
            Contract.Requires(tileFolder != null);

            using (FileStream fs = file.OpenRead())
            { return Deserialize(fs, tileFolder, isCompressed); }
        }
        public static MapInfo Deserialize(Stream stream, DirectoryInfo tileFolder, bool isCompressed = true)
        {
            Contract.Requires(stream != null);
            Contract.Requires(tileFolder != null);

            if (!tileFolder.Exists)
            { tileFolder.Create(); }
            foreach (FileInfo tile in tileFolder.GetFiles())
            { tile.Delete(); }
            MapInfoSerializeContainer container;
            if (isCompressed)
            {
                using (DeflateStream ds = new DeflateStream(stream, CompressionMode.Decompress))
                { container = Serializer.Deserialize<MapInfoSerializeContainer>(ds); }
            }
            else
            { container = Serializer.Deserialize<MapInfoSerializeContainer>(stream); }
            foreach (String tile in container.TileImages.Keys)
            {
                using (FileStream ts = new FileStream(Path.Combine(tileFolder.FullName, tile), FileMode.Create))
                {
                    byte[] bytes = container.TileImages[tile];
                    ts.Write(bytes, 0, bytes.Length);
                }
            }
            MapInfo map = new MapInfo();
            map.TileFolder = tileFolder;

            map.GridConfig.TileWidth = container.Grid.Width;
            map.GridConfig.TileHeight = container.Grid.Height;
            map.GridConfig.TileOffsetX = container.Grid.OffsetX;
            map.GridConfig.TileOffsetY = container.Grid.OffsetY;
            map.GridConfig.TileBorder = container.Grid.Border;

            map.PublicImage = LoadImage(container.PublicImage);
            map.SecretImage = LoadImage(container.SecretImage);

            Dictionary<MapInfoSerializeContainer.TileClass, Tile> convertedTiles = new Dictionary<MapInfoSerializeContainer.TileClass, Tile>();
            if (container.Tiles != null)
            {
                foreach (MapInfoSerializeContainer.TileClass serializedTile in container.Tiles)
                {
                    Tile tile = new Tile();
                    tile.X = serializedTile.X;
                    tile.Y = serializedTile.Y;
                    tile.IsEmptyPublic = serializedTile.EmptyPublic;
                    tile.IsEmptySecret = serializedTile.EmptySecret;
                    tile.HasSecret = serializedTile.HasSecret;
                    tile.IsRevealed = serializedTile.IsRevealed;
                    tile.ImageNamePublic = serializedTile.NamePublic;
                    tile.ImageNameSecret = serializedTile.NameSecret;
                    tile.LightLevel = serializedTile.LightLevel;
                    tile.LightSource = serializedTile.LightSource;
                    convertedTiles.Add(serializedTile, tile);
                    map.Tiles.Add(tile);
                }
                foreach (MapInfoSerializeContainer.TileClass serializedTile in container.Tiles)
                {
                    if (serializedTile.Adjacent == null)
                    { continue; }
                    Tile tile = convertedTiles[serializedTile];
                    foreach (MapInfoSerializeContainer.TileClass adjacent in serializedTile.Adjacent)
                    { tile.AdjacentTiles.Add(convertedTiles[adjacent]); }
                    if (serializedTile.NoLOS != null)
                    {
                        foreach (MapInfoSerializeContainer.TileClass noLOS in serializedTile.NoLOS)
                        { tile.NoLOSBetweenTiles.Add(convertedTiles[noLOS]); }
                    }
                }
            }
            int playerNum = 1;
            if (container.PlayerPositions != null)
            {
                foreach (MapInfoSerializeContainer.TileClass position in container.PlayerPositions)
                {
                    map.MovePlayer(playerNum++, convertedTiles[position]);
                }
            }
            return map;
        }

        public static void Serialize(MapInfo map, Stream destination, bool compress = true)
        {
            Contract.Requires(map != null);
            Contract.Requires(destination != null);
            MapInfoSerializeContainer container = new MapInfoSerializeContainer();
            container.Grid = new MapInfoSerializeContainer.GridClass();
            container.Tiles = new List<MapInfoSerializeContainer.TileClass>();
            container.TileImages = new Dictionary<string, byte[]>();
            container.PlayerPositions = new List<MapInfoSerializeContainer.TileClass>();

            container.Grid.Width = map.GridConfig.TileWidth;
            container.Grid.Height = map.GridConfig.TileHeight;
            container.Grid.OffsetX = map.GridConfig.TileOffsetX;
            container.Grid.OffsetY = map.GridConfig.TileOffsetY;
            container.Grid.Border = map.GridConfig.TileBorder;
            container.PublicImage = SaveImage(map.PublicImage);
            container.SecretImage = SaveImage(map.SecretImage);


            Dictionary<Tile, MapInfoSerializeContainer.TileClass> convertedTiles = new Dictionary<Tile, MapInfoSerializeContainer.TileClass>();
            foreach (Tile tile in map.Tiles)
            {
                MapInfoSerializeContainer.TileClass serializedTile = new MapInfoSerializeContainer.TileClass();
                serializedTile.Adjacent = new List<MapInfoSerializeContainer.TileClass>();
                serializedTile.NoLOS = new List<MapInfoSerializeContainer.TileClass>();

                serializedTile.X = tile.X;
                serializedTile.Y = tile.Y;
                serializedTile.EmptyPublic = tile.IsEmptyPublic;
                serializedTile.EmptySecret = tile.IsEmptySecret;
                serializedTile.HasSecret = tile.HasSecret;
                serializedTile.IsRevealed = tile.IsRevealed;
                serializedTile.NamePublic = tile.ImageNamePublic;
                serializedTile.NameSecret = tile.ImageNameSecret;
                serializedTile.LightLevel = tile.LightLevel;
                serializedTile.LightSource = tile.LightSource;
                convertedTiles.Add(tile, serializedTile);
                container.Tiles.Add(serializedTile);
            }
            foreach (Tile tile in map.Tiles)
            {
                MapInfoSerializeContainer.TileClass serializedTile = convertedTiles[tile];
                foreach (Tile adjacent in tile.AdjacentTiles)
                {
                    serializedTile.Adjacent.Add(convertedTiles[adjacent]);
                }
                foreach (Tile noLOS in tile.NoLOSBetweenTiles)
                {
                    serializedTile.NoLOS.Add(convertedTiles[noLOS]);
                }
            }
            foreach (Tile tile in map.PositionOfPlayers())
            { container.PlayerPositions.Add(convertedTiles[tile]); }

            DirectoryInfo dir = map.TileFolder;
            if (dir.Exists)
            {
                foreach (FileInfo file in dir.GetFiles())
                {
                    using (FileStream fs = file.OpenRead())
                    {
                        byte[] bytes = new byte[file.Length];
                        fs.Read(bytes, 0, bytes.Length);
                        container.TileImages.Add(file.Name, bytes);
                    }
                }
            }
            if (compress)
            {
                using (DeflateStream ds = new DeflateStream(destination, CompressionMode.Compress))
                { Serializer.Serialize<MapInfoSerializeContainer>(ds, container); }
            }
            else
            { Serializer.Serialize<MapInfoSerializeContainer>(destination, container); }
        }

        private static byte[] SaveImage(Image image)
        {
            if (image == null) { return null; }
            using (MemoryStream ms = new MemoryStream())
            {
                image.Save(ms, ImageFormat.Png);
                ms.Flush();
                return ms.ToArray();
            }
        }

        private static Image LoadImage(byte[] image)
        {
            if (image == null) { return null; }
            //The following ugly hack is a workaround for a known GDI issue that
            // occurs sometimes.  It messes up the image flags and causes an OutOfMemory 
            // exception when attempting to draw the image.  It's not actual out of memory
            // it's just a really bad useless error message.  The solution is to clone the
            // image after loading and dispose the original.
            // http://social.msdn.microsoft.com/forums/en-US/netfxbcl/thread/2b813a2e-321b-46ae-9446-47d5b5a82eea
            Image temp;
            using (MemoryStream ms = new MemoryStream(image))
            { temp = Image.FromStream(ms); }
            Image result = new Bitmap(temp);
            temp.Dispose();
            return result;
        }

        public static void Serialize(MapInfo map, FileInfo destination, bool compress = true)
        {
            using (FileStream output = destination.OpenWrite())
            { Serialize(map, output, compress); }
        }

        public static void Serialize(MapInfo map, String destination, bool compress = true)
        { Serialize(map, new FileInfo(destination), compress); }

        [ProtoContract]
        private class MapInfoSerializeContainer
        {
            [ProtoMember(1, IsRequired = true)]
            public GridClass Grid { get; set; }
            [ProtoMember(2, AsReference = true)]
            public List<TileClass> PlayerPositions { get; set; }
            [ProtoMember(3, AsReference = true, IsRequired = true)]
            public List<TileClass> Tiles { get; set; }
            [ProtoMember(4, IsRequired = true)]
            public byte[] PublicImage { get; set; }
            [ProtoMember(5)]
            public byte[] SecretImage { get; set; }
            [ProtoMember(6, IsRequired = true)]
            public Dictionary<String, byte[]> TileImages { get; set; }

            [ProtoContract]
            public class GridClass
            {
                [ProtoMember(1)]
                public int Width { get; set; }
                [ProtoMember(2)]
                public int Height { get; set; }
                [ProtoMember(3)]
                public int OffsetX { get; set; }
                [ProtoMember(4)]
                public int OffsetY { get; set; }
                [ProtoMember(5)]
                public int Border { get; set; }
            }

            [ProtoContract]
            public class TileClass
            {
                [ProtoMember(1)]
                public int X { get; set; }
                [ProtoMember(2)]
                public int Y { get; set; }
                [ProtoMember(3)]
                public String NamePublic { get; set; }
                [ProtoMember(4)]
                public String NameSecret { get; set; }
                [ProtoMember(5)]
                public bool EmptyPublic { get; set; }
                [ProtoMember(6)]
                public bool EmptySecret { get; set; }
                [ProtoMember(7)]
                public bool HasSecret { get; set; }
                [ProtoMember(8)]
                public bool IsRevealed { get; set; }
                [ProtoMember(9)]
                public int LightLevel { get; set; }
                [ProtoMember(10)]
                public int LightSource { get; set; }
                [ProtoMember(11, AsReference = true)]
                public List<TileClass> Adjacent { get; set; }
                [ProtoMember(12, AsReference = true)]
                public List<TileClass> NoLOS { get; set; }
            }
        }
    }
}
